﻿using BuildExplorer.Properties;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace BuildExplorer
{
    internal partial class BuildExplorerForm : Form
    {
        IBuildDetail build;
        DateTime buildRefreshTime;

        private OpenBuild openBuild = new OpenBuild();
        private RichTextNode richTextNode;

        private Action<string> afterUpdate = null;

        public CommandLine commandLine;

        public BuildExplorerForm()
        {
            InitializeComponent();

            richTextNode = new RichTextNode(richTextBox1);
        }

        # region Build Load

        protected override async void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            PopulateTimeZone();

            if (!string.IsNullOrEmpty(commandLine.CollectionUrl) && !string.IsNullOrEmpty(commandLine.buildUriOrName))
            {
                try
                {
                    build = await Task.Run(() => GetBuildFromCommandLine());
                    LoadBuildDetail();
                }
                catch (Exception ex)
                {
                    ShowError(ex);
                }
            }
            else
            {
                _btnOpenBuild_Click(this, e);
            }
        }

        private IBuildDetail GetBuildFromCommandLine()
        {
            var collection = new TfsTeamProjectCollection(new Uri(commandLine.CollectionUrl));
            var buildServer = collection.GetService<IBuildServer>();
            if (commandLine.buildUriOrName.StartsWith("vstfs://"))
            {
                return buildServer.GetBuild(new Uri(commandLine.buildUriOrName),
                    informationTypes: null,
                    queryOptions: QueryOptions.Definitions | QueryOptions.BatchedRequests);
            }
            else
            {
                var spec = buildServer.CreateBuildDefinitionSpec(teamProject: "*", definitionName: "*", propertyNameFilters: null);
                return buildServer.GetBuild(spec, 
                    commandLine.buildUriOrName,
                    informationTypes: null,
                    queryOptions: QueryOptions.Definitions | QueryOptions.BatchedRequests);
            }
        }

        private void OpenBuild(string requestedBuild, bool OpenInNewInstance = false)
        {
            openBuild.RequestedBuild = requestedBuild;
            if (openBuild.ShowDialog() == DialogResult.OK)
            {
                if (openBuild.OkKeys == Keys.Control)
                {
                    OpenInNewInstance = true;
                }

                if (OpenInNewInstance)
                {
                    string myCmd = System.Reflection.Assembly.GetEntryAssembly().Location;
                    Process.Start(myCmd, CommandLine.GetForBuild(openBuild.SelectedBuild));
                }
                else
                {
                    build = openBuild.SelectedBuild;
                    LoadBuildDetail();
                }
            }
        }

        private void _txtBuild_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyData == Keys.Enter)
            {
                OpenBuild(_txtBuild.Text);

                e.SuppressKeyPress = true;
                e.Handled = true;
            }
            else if (e.KeyData == (Keys.Enter | Keys.Control))
            {
                OpenBuild(_txtBuild.Text, OpenInNewInstance: true);
                _txtBuild.Text = (build==null) ? string.Empty : build.BuildNumber;

                e.SuppressKeyPress = true;
                e.Handled = true;
            }
        }

        private void _btnOpenBuild_Click(object sender, EventArgs e)
        {
            OpenBuild(requestedBuild: null);
        }

        private void LoadBuildDetail()
        {
            Clear();

            this.Text = string.Format("Build Explorer [{0} - {1}]", Util.GetDefinitionName(build.BuildDefinition), build.BuildNumber);
            _txtBuild.Text = build.BuildNumber;
            _textBoxBuildId.Text = build.Uri.ToString().Split('/').Last();

            RefreshBuildDetail();

            _btnAutoRefresh.Checked =
                _tmrAutoRefresh.Enabled = false;  // !build.BuildFinished; // Don't do this - refresh is too slow
        }

        private async void RefreshBuildDetail()
        {
            if (build == null)
            {
                return;
            }

            this.Cursor = Cursors.WaitCursor;

            _txtLog.AppendText(string.Format("{0} - Loading...\r\n", build.BuildNumber));

            buildRefreshTime = DateTime.UtcNow;
            var stopwatchLoad = Stopwatch.StartNew();
            try
            {
                await Task.Run(() => build.RefreshAllDetails());
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }

            LogAgentsAndWorkspaces(build.Information);

            _treeView1.BeginUpdate();
            _treeView1.RefreshBuildDetail(build);
            _treeView1.EndUpdate();
            stopwatchLoad.Stop();

            _txtLog.AppendText(string.Format("{0} - Load time: {1:F2} seconds\r\n\r\n", build.BuildNumber, stopwatchLoad.ElapsedMilliseconds / 1000.0));

            UpdateStatusBar();

            _btnStop.Enabled = !build.BuildFinished;

            // Disable refresh when the build is finished.
            _btnAutoRefresh.Checked =
                _tmrAutoRefresh.Enabled = _tmrAutoRefresh.Enabled && !build.BuildFinished;

            if (_treeView1.Nodes.Count > 0)
            {
                _treeView1.SelectedNode = _treeView1.Nodes[0];
            }

            if (!build.BuildFinished)
            {
                _btnExpandErrors_Click(null, null);
            }

            this.Cursor = Cursors.Default;
        }

        private void UpdateStatusBar()
        {
            if (build != null)
            {
                _lblStatus.Text = string.Format("Status: {0}", build.Status);

                _lblLastRefreshed.Text = string.Format("Refreshed: {0}", Util.FormatDateTime(buildRefreshTime));
                _lblLastRefreshed.Visible = !build.BuildFinished;

                _lblStarted.Text = string.Format("Started: {0}", Util.FormatDateTime(build.StartTime.ToUniversalTime()));
                _lblFinished.Text = string.Format("Finished: {0}", Util.FormatDateTime(build.FinishTime.ToUniversalTime(), buildRefreshTime - build.StartTime.ToUniversalTime()));
                //            _lblFinished.Visible = build.BuildFinished;

                _btnDropLocation.Tag = build.DropLocation;
                _btnDropLocation.Enabled = !string.IsNullOrEmpty(build.DropLocation) && Directory.Exists(build.DropLocation);
            }
        }

        /// <summary>
        /// Search in the build log for machines that were in use
        /// </summary>
        /// <param name="info">The specific build information</param>
        private void LogAgentsAndWorkspaces(IBuildInformation info)
        {
            foreach (var node in info.Nodes)
            {
                if (node.Type == "ActivityProperties")
                {
                    continue;
                }

                //string s = node.Fields.ContainsKey("DisplayText") ? node.Fields["DisplayText"] :
                //           node.Fields.ContainsKey("ActivityType") ? node.Fields["ActivityType"] :
                //           node.Type == "BuildMessage" ? node.Fields["Message"] :
                //           node.Type;
                //System.Diagnostics.Trace.WriteLine(s);

                if (node.Type == "AgentScopeActivityTracking")
                {
                    _txtLog.AppendText(string.Format("AgentScope '{0}': {1}, {2}, {3}\r\n",
                        node.Fields["DisplayText"],
                        node.Fields["ReservedAgentName"],
                        Util.FormatValue(node.Fields["StartTime"]),
                        node.GetDuration()));
                }
                else if (node.Fields.ContainsKey("ActivityType"))
                {
                    switch (node.Fields["ActivityType"])
                    {
                        case "Microsoft.TeamFoundation.Build.Workflow.Activities.CreateWorkspace":
                            var childNodes = node.Children.Nodes;
                            if (childNodes.Any() && childNodes.Last().Fields.ContainsKey("Name"))
                            {
                                var workspaceName = childNodes.Last().Fields["Name"];
                                _txtLog.AppendText(string.Format("Workspace Name: {0} \r\n", workspaceName));
                            }
                            break;

                        case "Microsoft.TeamFoundation.Build.Workflow.Activities.SyncWorkspace":
                            continue;  // GetWorkspace can have many children which are irrelevant to us - skip it to save time
                    }
                }

                LogAgentsAndWorkspaces(node.Children);
            }
        }

        private void Clear()
        {
            _treeView1.BeginUpdate();
            _treeView1.Nodes.Clear();
            richTextBox1.Clear();
            _treeView1.EndUpdate();

            this.Text = Properties.Resources.Title;
        }

        #endregion

        #region ToolStrip buttons

        private void _txtSearch_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyData == (Keys.Enter | Keys.Shift))
            {
                SearchPrevious();
                _txtSearch.Focus();
                e.Handled = true;
            }
            else if (e.KeyData == Keys.Enter)
            {
                SearchNext();
                _txtSearch.Focus();
                e.Handled = true;
            }
        }

        private void SearchNextClick(object sender, EventArgs e)
        {
            SearchNext();
        }

        private void SearchNext()
        {
            bool found = richTextNode.SearchNext(_txtSearch.Text);
            if (found)
            {
                ShowFound();
            }
            else
            {
                bool foundNode = _treeView1.SelectNextMatch(tn => tn.Contains(_txtSearch.Text));
                if (foundNode)
                {
                    afterUpdate = (text) => richTextNode.SearchNext(text);
                }
                else
                {
                    ShowNotFound();
                }
            }
        }

        private void SearchPreviousClick(object sender, EventArgs e)
        {
            SearchPrevious();
        }

        private void SearchPrevious()
        {
            bool found = richTextNode.SearchPrev(_txtSearch.Text);
            if (found)
            {
                ShowFound();
            }
            else
            {
                bool foundNode = _treeView1.SelectPreviousMatch(tn => tn.Contains(_txtSearch.Text));
                if (foundNode)
                {
                    afterUpdate = (text) => richTextNode.SearchPrevFromEnd(text);
                }
                else
                {
                    ShowNotFound();
                }
            }
        }

        private void _txtSearch_TextChanged(object sender, EventArgs e)
        {
            ShowFound();

            // Don't highlight synchroneously - will delay entering keys when there's a lot of text
            // Instead, schedule to run after 1 millisecond - additional queued TextChanged events will cancel and re-schedule, so highlight runs only in a "quiet period".
            performSearch.Stop();
            performSearch.Interval = 1;
            performSearch.Start();
        }

        private void ShowNotFound()
        {
            _txtSearch.ForeColor = Color.Red;
            _txtSearch.BackColor = Color.FromArgb(0xFF, 0xFF, 0xE0, 0x80);
            System.Media.SystemSounds.Beep.Play();
        }

        private void ShowFound()
        {
            _txtSearch.ForeColor = SystemColors.WindowText;
            _txtSearch.BackColor = SystemColors.Window;
        }

        private void performSearch_Tick(object sender, EventArgs e)
        {
            richTextNode.Highlight(_txtSearch.Text);
            performSearch.Stop();
        }

        private void _btnRefresh_Click(object sender, EventArgs e)
        {
            RefreshBuildDetail();
        }

        private void AutoRefreshClick(object sender, EventArgs e)
        {
            _tmrAutoRefresh.Enabled = _btnAutoRefresh.Checked;
        }

        private void AutoRefreshTimerTick(object sender, EventArgs e)
        {
            RefreshBuildDetail();
        }

        private void StopClick(object sender, EventArgs e)
        {
            if (build != null)
            {
                var res = MessageBox.Show(this, "Are you sure you want to stop this build?", "Stop build " + build.BuildNumber, MessageBoxButtons.YesNo);
                if (res == System.Windows.Forms.DialogResult.Yes)
                {
                    BusyWait(() => build.Stop());
                }
            }
        }

        private void DropLocationClick(object sender, EventArgs e)
        {
            string drop = _btnDropLocation.Tag as string;
            if (!string.IsNullOrEmpty(drop))
            {
                BusyWait(() => Process.Start(drop));
            }
        }

        private void _btnPreviousError_Click(object sender, EventArgs e)
        {
            _treeView1.SelectPreviousMatch(tn => tn.IsErrorOrExecuting());
        }

        private void _btnNextError_Click(object sender, EventArgs e)
        {
            _treeView1.SelectNextMatch(tn => tn.IsErrorOrExecuting());
        }

        private void _btnExpandErrors_Click(object sender, EventArgs e)
        {
            _treeView1.ExpandAllMatches(tn => tn.IsErrorOrExecuting());
        }

        private void toolStripButton1_Click(object sender, EventArgs e)
        {
            openAsLogFileToolStripMenuItem_Click(sender, e);
        }

        private void PopulateTimeZone()
        {
            foreach (var clock in Clock.Clocks)
            {
                _toolStripTimeZone.DropDownItems.Add(new ToolStripMenuItem(clock.Value.GetFullDisplayName()) { Tag = clock.Key });
            }

            ShowTimeZone();
        }

        private void _toolStripTimeZone_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            Settings.Default.TzRegKeyName = (string)e.ClickedItem.Tag;
            Settings.Default.Save();
            ShowTimeZone();

            UpdateStatusBar();
            if (this._treeView1.SelectedNode != null)
            {
                BeginInvoke((MethodInvoker)delegate()
                {
                    richTextNode.Update((IBuildInformationNode)this._treeView1.SelectedNode.Tag, _txtSearch.Text, afterUpdate: null);
                });
            }
        }

        private void ShowTimeZone()
        {
            _toolStripTimeZone.Text = Clock.GetClock(Settings.Default.TzRegKeyName).DisplayName;
        }

        #endregion

        #region Main form behaviors

        /// <summary>
        /// Override to handle specific shortcuts
        /// </summary>
        /// <param name="msg">A <see cref="T:System.Windows.Forms.Message"/>, passed by reference, that represents the Win32 message to process.</param>
        /// <param name="keyData">One of the <see cref="T:System.Windows.Forms.Keys"/> values that represents the key to process.</param>
        /// <returns>
        /// true if the keystroke was processed and consumed by the control; otherwise, false to allow further processing.
        /// </returns>
        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            // Search previous
            if (keyData == (Keys.F3 | Keys.Shift))
            {
                SearchPrevious();
                return true;
            }

            // Search next
            if (keyData == Keys.F3)
            {
                SearchNext();
                return true;
            }

            // Refresh
            if (keyData == Keys.F5)
            {
                RefreshBuildDetail();
                return true;
            }

            // CTRL+F for search
            if (keyData == (Keys.F | Keys.Control))
            {
                _txtSearch.SelectAll();
                _txtSearch.Focus();
                return true;
            }

            return base.ProcessCmdKey(ref msg, keyData);
        }

        private void _treeView1_KeyDown(object sender, KeyEventArgs e)
        {
            Func<string> data = () => _treeView1.SelectedNode.Text;
            CopyToClipBoard(e, data);
        }

        private static void CopyToClipBoard(KeyEventArgs e, Func<string> input)
        {
            if (e.KeyData == (Keys.Control | Keys.C))
            {
                var data = input();
                if (!string.IsNullOrEmpty(data))
                {
                    int retryCount = 2;
                    while (retryCount > 0)
                    {
                        try
                        {
                            Clipboard.Clear();
                            Clipboard.SetDataObject(data);
                            break;
                        }
                        catch (System.Runtime.InteropServices.ExternalException)
                        {
                            Thread.Sleep(500);
                            retryCount--;
                        }
                    }
                }
            }
        }

        private void TreeNodeAfterSelect(object sender, TreeViewEventArgs e)
        {
            richTextNode.Update(e.Node.Tag as IBuildInformationNode, _txtSearch.Text, afterUpdate);
            afterUpdate = null;
        }

        private void exportToFileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (this.saveFD.ShowDialog() != DialogResult.Cancel)
            {
                SaveToFile(this.saveFD.FileName);
            }
        }

        private void openAsLogFileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (build == null)
            {
                return;
            }

            string filename = "BuildExplorer_" + build.BuildNumber + "_" + this._treeView1.SelectedNode.Text;

            foreach (var c in Path.GetInvalidFileNameChars()) { filename = filename.Replace(c, '-'); }

            string filePath = Path.Combine(Path.GetTempPath(), filename);
            filePath = filePath.Truncate(248) + ".log"; // Avoid PathTooLongException

            SaveToFile(filePath);

            Process.Start(filePath);
        }

        private void SaveToFile(string filePath)
        {
            using (var outFile = new StreamWriter(filePath))
            {
                // Note: Writes text of the selected node, rather than the node which the user right-clicked on. Not ideal, but easier to implement...
                outFile.Write(richTextBox1.Text);
            }
        }

        private void richTextBox1_LinkClicked(object sender, LinkClickedEventArgs e)
        {
            Process.Start(e.LinkText);
        }

        private void toolStripMenuItemClear_Click(object sender, EventArgs e)
        {
            _txtLog.Clear();
        }

        #endregion

        private void BusyWait(Action a)
        {
            try
            {
                this.Cursor = Cursors.WaitCursor;
                a();
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
            finally
            {
                this.Cursor = Cursors.Default;
            }
        }

        private void ShowError(Exception e)
        {
            MessageBox.Show(this, e.Message,
                Resources.Title,
                MessageBoxButtons.OK,
                MessageBoxIcon.Error,
                MessageBoxDefaultButton.Button1,
                (MessageBoxOptions)0);
        }
    }
}
